enhancement(utxo): improve trade and withdraw fee calculations#2083
enhancement(utxo): improve trade and withdraw fee calculations#2083borngraced wants to merge 20 commits intodevfrom
Conversation
|
@borngraced please resolve conflicts in this PR |
mariocynicys
left a comment
There was a problem hiding this comment.
Early review.
Will do another round once this PR is r2r.
Have a Q though: Why did we add tx_size in trade fee? Why do we keep tx_size around? It would be helpful having these answers in the PR header post, and possibly explaining the approach taken.
| match try_s!(self.get_tx_fee().await) { | ||
| ActualTxFee::Dynamic(amount) | ActualTxFee::FixedPerKb(amount) => Ok(amount + gas_fee), | ||
| } | ||
| Ok(try_s!(self.get_tx_fee_per_kb().await) + gas_fee) |
There was a problem hiding this comment.
do we want to add a fee rate (rate per kb) to the gas fee?
Is the gas fee here a rate as well (per kb)?
There was a problem hiding this comment.
assuming that gas_fee represents the gas costs and self.get_tx_fee_per_kb() represents the per-kb costs associated with the tx size
shamardy
left a comment
There was a problem hiding this comment.
Thank you for the PR! First review iteration from my side where I discuss some modifications and proposals on how we can handle fee priority in a better way!
mm2src/coins/utxo/utxo_withdraw.rs
Outdated
| /// Num of blocks applied to withdrawal transactions | ||
| /// that have a high priority, indicating a need for faster confirmation. | ||
| pub const HIGH_TX_FEE: u8 = 1; | ||
|
|
||
| /// Num of blocks applied to withdrawal transactions | ||
| /// that have a normal priority, indicating a moderate confirmation time. | ||
| pub const NORMAL_TX_FEE: u8 = 2; | ||
|
|
||
| /// Num of blocks applied to withdrawal transactions | ||
| /// that have a low priority, indicating a longer confirmation time. | ||
| pub const LOW_TX_FEE: u8 = 4; |
There was a problem hiding this comment.
I have some concerns about the choice of the number of blocks used for transaction confirmation priority.
For high fee transactions, we could consider using 2 blocks. This is based on the below https://github.com/KomodoPlatform/komodo-defi-framework/blob/9ec826a1c0f74664cce4e0cb383b79455335aec1/mm2src/coins/utxo/rpc_clients.rs#L2012-L2016 However, this might only be applicable to some UTXO coins and not all. For Bitcoin, the situation might be different. Therefore, it might be beneficial to get these values from the coin config where they are tested separately for each coin that supports fee estimation.
For normal fee transactions, 6 blocks seem to be a good choice. This implies that we expect the transaction to be confirmed within an hour for Bitcoin.
For low fee transactions, the goal should be to choose the lowest possible fee that will still get the transaction confirmed and not removed from the mempool. This is easier said than done. The mempool explorer provides a good API for estimating this for BTC only. It's the economyFee field, which is the lower of the low priority (1 hour fee) or 2x the minimum allowed fee.
I recommend testing the value generated from Electrum's blockchain.estimatefee against the values from the mempool explorer. The mempool explorer is one of the most reliable explorers for BTC fees. If we can't estimate good fees using Electrum for BTC, we can use the mempool API for BTC only. Although it's rate-limited, it will be rate-limited per user which is acceptable.
Lastly, please also modify the below doc comments to reflect the number of blocks used. https://github.com/KomodoPlatform/komodo-defi-framework/blob/9ec826a1c0f74664cce4e0cb383b79455335aec1/mm2src/coins/lp_coins.rs#L1916-L1925
P.S. We can continue this discussion on DM since it might be a bit complicated.
There was a problem hiding this comment.
This is how mempool explorer calculates the different fee levels https://github.com/mempool/mempool/blob/db34ca6a5f37f8c420ce961f0ccd96bba9205dfd/backend/src/api/fee-api.ts#L22-L63 I guess High = 1 , Normal = 2, Low = 3 or 4 is applicable to bitcoin only and not other UTXOs.
There was a problem hiding this comment.
Only last note :)
In the future we can enable RBF and use lower fees for the low level while giving the ability to the user to bump the fee if it's not confirming in time. This is out of scope of this PR ofcourse.
There was a problem hiding this comment.
In the future we can enable RBF and use lower fees for the low level while giving the ability to the user to bump the fee if it's not confirming in time. This is out of scope of this PR ofcourse.
Didnt find such issue, so created it #2327
| /// encapsulates the priority of a withdrawal transaction, indicating the desired fee | ||
| /// level for transaction processing. | ||
| UtxoPriority { | ||
| priority: UtxoFeePriority, | ||
| }, |
There was a problem hiding this comment.
Here is how I envision the GUI implementation of this part.
We present the user with the three levels of fees and let them make a choice. The GUI would potentially make three separate withdrawal requests, each using a different priority level. This would generate three different raw transaction hexes, each associated with a different fee and expected confirmation time.
Once the user selects the fee they prefer, the corresponding transaction hex would be used.
What are your thoughts on this approach? Would it be feasible to make some changes that simplifies the process for the GUI to handle this with a single request?
There was a problem hiding this comment.
Okay, here's mine view.
The api receives a priority withdrawal request from the gui, the api generates fee for the available priorities(currently 3) and send the response (each fee with it's tx hex) and finally, gui can render this three options for user to pick a choice.
There was a problem hiding this comment.
The api receives a priority withdrawal request from the gui, the api generates fee for the available priorities(currently 3) and send the response (each fee with it's tx hex) and finally, gui can render this three options for user to pick a choice.
This is how I see it as well, but it might be too complicated as we would probably need to return 3 TransactionDetails objects in the response which is different from the current withdraw response.
There was a problem hiding this comment.
FYI for eth I also added priority levels for tx fees:
For withdrawals user can add a new WithdrawFee option and set explicit fee per gas.
For swaps there is an rpc to set the desired level of low/medium/priority for the whole swap (so the API will by itself query the current fee value before each swap step).
There is also an rpc for GUI to query fees for all supported 3 levels (to show fe values to the user so he could select desired level). Initially user should start a loop in the API which periodically calculates priority fees, so the query rpc just immediately returns the recent estimated fee values.
Maybe we could do this similar both for utxo and eth.
There was a problem hiding this comment.
(Some explanation why a there is loop in API to obtain fees. This is how other eth wallet apps do it: API does fee estimations getting data from the eth provider. The GUI subscribes to the fee updates from API so it can in realtime notify the user about changes)
| .compat() | ||
| .await?; | ||
|
|
||
| if ["BTC"].contains(&coin.as_ref().conf.ticker.as_str()) { |
There was a problem hiding this comment.
Is this intended to check tickers like that?
I guess this would be false for coins like "BTC-segwit"
(Maybe consider adding a config param?)
| .await | ||
| ); | ||
| if fee >= payment_value { | ||
| if fee.fee >= payment_value { |
There was a problem hiding this comment.
when I was studying mm2 code I messed up with used several 'fee' concepts. So I'd suggest we use 'qualified' names for fees, if possible, like 'tx_fee' or 'dex_fee' (not just 'fee') to easy get what fee is meant
|
|
||
| #[derive(Debug)] | ||
| pub struct HtlcSpendFeeResult { | ||
| pub fee: u64, |
| async fn generate_withdraw_fee_using_priority<C: UtxoCommonOps + GetUtxoListOps>( | ||
| coin: &C, | ||
| priority: &UtxoFeePriority, | ||
| priorities: &Option<UtxoFeePriorities>, |
There was a problem hiding this comment.
do we need 'priorities' param? Could we not get it from the 'coin' param?
|
|
||
| /// Priority levels for UTXO fee estimation for withdrawal. | ||
| #[derive(Clone, Debug, Deserialize)] | ||
| pub struct UtxoFeePriorities { |
There was a problem hiding this comment.
It seems to me UtxoFeePriorities does not exactly matches the concept.
How about
#[derive(Clone, Debug, Deserialize)]
struct UtxoTxWaitBlocks {
pub low_priority: u8,
pub normal_priority: u8,
pub high_priority: u8,
}
In the coins file the param could have a name like blocks_tx_to_wait or tx_wait_blocks
| fn avg_blocktime(&self) -> Option<u64> { self.conf["avg_blocktime"].as_u64() } | ||
|
|
||
| fn fee_priorities(&self) -> Option<UtxoFeePriorities> { | ||
| json::from_value(self.conf["fee_priorities"].clone()).unwrap_or(None) |
There was a problem hiding this comment.
Suggestion: we could throw a error if 'fee_priorities' is parsed incorrectly (bad or omitted values)
|
I think I have a general question: |
|
ps @borngraced I see that PR lint fails |
UTXO Fee Improvements:
Withdraw using
UtxoPriority::Low{ "userpass": "SamopE160!", "method": "withdraw", "mmrpc": "2.0", "params": { "to": "RCKfmv1X4oZHhGgb9mVD8XnkubAerWEcQ4", "max": false, "coin": "DOC", "amount": "1000", "fee": { "type": "UtxoPriority", "priority": "Low", } } }#1835